package codediff.service;

import codediff.CodeDiff;
import codediff.GitAdapter;
import codediff.IncrClassInfo;
import codediff.controller.CodeSaveDto;
import codediff.parse.report.*;
import codediff.parse.report.enumeration.CounterType;
import codediff.repository.CoverageReportRepository;
import com.google.common.collect.ImmutableList;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static codediff.parse.XmlParseUtil.convertXmlToJson;

@Service
public class CodeService {

    @Autowired
    private CoverageReportRepository coverageReportRepository;

    public void get(String repoPath, String baseLabel, String currentLabel) {

    }

    public void save(CodeSaveDto codeSaveDto) throws GitAPIException {
//        CoverageReport coverageReport = convertXmlToJson("G:\\workspace\\oo-tdd-ydx\\execs\\normal.xml").getCoverageReport().get(0);
//        coverageReport.getPackages().forEach(CoveragePackage::mergeClass);
//
//        CodeDiff codeDiff = new CodeDiff(new GitAdapter("G:\\workspace\\oo-tdd-ydx"));
//        List<IncrClassInfo> classInfoList = codeDiff.diffTags("132b93c8", "9c8aa791");

        CoverageReport coverageReport = convertXmlToJson(codeSaveDto.getXmlPath()).getCoverageReport().get(0);
        coverageReport.getPackages().forEach(CoveragePackage::mergeClass);
        CodeDiff codeDiff = new CodeDiff(new GitAdapter(codeSaveDto.getRepoPath()));
        List<IncrClassInfo> classInfoList = codeDiff.diffTags(codeSaveDto.getBaseLabel(), codeSaveDto.getCurrentLabel());

        fillLinesProperty(coverageReport, classInfoList);

        setClassNewPackage(coverageReport);
        setPackageNewCoverage(coverageReport);
        setReportNewCoverage(coverageReport);

        coverageReport.setRepoPath(codeSaveDto.getRepoPath());
        coverageReport.setBaseLabel(codeSaveDto.getBaseLabel());
        coverageReport.setCurrentLabel(codeSaveDto.getCurrentLabel());

        coverageReportRepository.save(coverageReport).block();
    }

    private void fillLinesProperty(CoverageReport coverageReport, List<IncrClassInfo> classInfoList) {
        Map<String, Map<String, CoverageMergeClass>> packageClassMap = coverageReport.getPackages().stream()
                .collect(Collectors.toMap(CoveragePackage::getName, coveragePackage -> coveragePackage.getCoverageMergeClasses().stream()
                        .collect(Collectors.toMap(coverageMergeClass -> coverageMergeClass.getSourcefilename().replace(".java", ""),
                                mergeClass -> mergeClass))));
        classInfoList.stream()
                .filter(incrClassInfo -> packageClassMap.containsKey(incrClassInfo.getPackageName()))
                .filter(incrClassInfo -> packageClassMap.get(incrClassInfo.getPackageName()).containsKey(incrClassInfo.getClassName()))
                .forEach(incrClassInfo -> {
                    CoverageMergeClass mergeClass = packageClassMap.get(incrClassInfo.getPackageName()).get(incrClassInfo.getClassName());
                    mergeClass.getCoverageLines().stream()
                            .filter(coverageLine -> incrClassInfo.getAddLines().contains(coverageLine.getNr())
                                    || incrClassInfo.getChangeType().equals(DiffEntry.ChangeType.ADD.name()))
                            .forEach(coverageLine -> coverageLine.setNewLine(true));
                });
    }

    private void setClassNewPackage(CoverageReport coverageReport) {
        coverageReport.getPackages().forEach(coveragePackage -> {
            coveragePackage.getCoverageMergeClasses().forEach(mergeClass -> {
                List<CoverageLine> newLines = mergeClass.getCoverageLines().stream()
                        .filter(CoverageLine::isNewLine)
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(newLines)) {
                    return;
                }
                List<Counter> newCoverageCounters = new ImmutableList.Builder<Counter>()
                        .add(Counter.newInstruction(newLines))
                        .add(newMethod(incrClassInfo, mergeClass.getCoverageMethods(), newLines))
                        .add(Counter.newLine(newLines))
                        .add(Counter.newBranch(newLines))
                        .add(Counter.newClass(mergeClass))
                        .build();
                mergeClass.getCounters().addAll(newCoverageCounters);
            });
        });
    }

    private static Counter newMethod(IncrClassInfo incrClassInfo, List<CoverageMethod> coverageMethods, List<CoverageLine> newLines) {
        List<CoverageMethod> newCoveredMethods = updateAndGetNewMethodCoverage(incrClassInfo, coverageMethods, newLines);
        return new Counter(CounterType.NEW_METHOD,
                incrClassInfo.getIncrMethodInfos().size() - newCoveredMethods.size(), newCoveredMethods.size());
    }

    private static List<CoverageMethod> updateAndGetNewMethodCoverage(IncrClassInfo incrClassInfo,
                                                                      List<CoverageMethod> coverageMethods,
                                                                      List<CoverageLine> newLines) {
        List<Integer> methodCoveredLines = getMethodCoveredLines(incrClassInfo, newLines);
        return coverageMethods.stream()
                .peek(coverageMethod -> {
                    if (methodCoveredLines.contains(coverageMethod.getLine())) {
                        coverageMethod.getCounters().add(new Counter(CounterType.NEW_METHOD, 0, 1));
                    } else {
                        coverageMethod.getCounters().add(new Counter(CounterType.NEW_METHOD, 1, 0));
                    }
                })
                .filter(coverageMethod -> methodCoveredLines.contains(coverageMethod.getLine()))
                .collect(Collectors.toList());
    }

    private static List<Integer> getMethodCoveredLines(IncrClassInfo incrClassInfo, List<CoverageLine> newLines) {
        List<Integer> newCoveredLines = newLines.stream()
                .filter(it -> (it.getCi() + it.getCb()) > 0)
                .map(CoverageLine::getNr)
                .collect(Collectors.toList());
        return incrClassInfo.getIncrMethodInfos().stream()
                .map(incrMethodInfo -> {
                    List<Integer> includeLines = incrMethodInfo.getIncludeLines();
                    return includeLines.stream().anyMatch(newCoveredLines::contains) ? includeLines : null;
                })
                .filter(CollectionUtils::isNotEmpty)
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }

    private void setPackageNewCoverage(CoverageReport coverageReport) {
        coverageReport.getPackages().forEach(coveragePackage -> {
            List<Counter> newCounters = getNewCoverageCounter(getClassCounterStream(coveragePackage));
            coveragePackage.getCounters().addAll(newCounters);
        });
    }

    private Stream<Counter> getClassCounterStream(CoveragePackage coveragePackage) {
        return coveragePackage.getCoverageMergeClasses().stream().map(CoverageMergeClass::getCounters).flatMap(List::stream);
    }

    private void setReportNewCoverage(CoverageReport coverageReport) {
        List<Counter> newCounters = getNewCoverageCounter(getPackageCounterStream(coverageReport));
        coverageReport.getCounters().addAll(newCounters);
    }

    private Stream<Counter> getPackageCounterStream(CoverageReport coverageReport) {
        return coverageReport.getPackages().stream().map(CoveragePackage::getCounters).flatMap(List::stream);
    }

    private List<Counter> getNewCoverageCounter(Stream<Counter> counterStream) {
        Map<CounterType, Pair<Integer, Integer>> newCountMap = counterStream
                .filter(counter -> CounterType.isNewCounterType(counter.getType()))
                .collect(Collectors.toMap(Counter::getType, it -> Pair.of(it.getMissed(), it.getCovered()), (pairA, pairB) ->
                        Pair.of(pairA.getLeft() + pairB.getLeft(), pairA.getRight() + pairB.getRight())));
        return newCountMap.entrySet().stream()
                .map(entry -> new Counter(entry.getKey(), entry.getValue().getLeft(), entry.getValue().getRight()))
                .collect(Collectors.toList());
    }

    public Mono<CoverageReport> getReport(String repoPath, String baseLabel, String currentLabel) {
        return coverageReportRepository.findByRepoPathAndCurrentLabelAndBaseLabel(repoPath, currentLabel, baseLabel)
                .defaultIfEmpty(new CoverageReport());
    }

    public Mono<List<Counter>> test1(String repoPath, String baseLabel, String currentLabel) {
        return coverageReportRepository.findByRepoPathAndCurrentLabelAndBaseLabel(repoPath, currentLabel, baseLabel)
                .map(CoverageReport::getPackages)
                .map(coveragePackages -> coveragePackages.stream().map(CoveragePackage::getCoverageMergeClasses)
                        .flatMap(List::stream).collect(Collectors.toList()))
                .map(mergeClasses -> mergeClasses.stream().map(CoverageMergeClass::getCounters).flatMap(List::stream))
                .map(counter -> counter.filter(it -> CounterType.isNewCounterType(it.getType())).collect(Collectors.toList()))
                .defaultIfEmpty(Collections.emptyList());
    }
}
